{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "940844d8",
   "metadata": {},
   "source": [
    "在minist手写数字上训练 Marcov Hierachical VAE 图像生成模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "5473f49f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torchvision as tv\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "2e2b6cf6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 超参数定义\n",
    "from dataclasses import dataclass\n",
    "import os\n",
    "\n",
    "@dataclass\n",
    "class Config:\n",
    "    # 文件&数据\n",
    "    data_path: str = \"./dataset/mnist\"\n",
    "    save_dir: str = \"./checkpoints\"\n",
    "    save_every: int = 10\n",
    "    transform = tv.transforms.Compose([\n",
    "        tv.transforms.ToTensor(),\n",
    "        # tv.transforms.Normalize((0.5,), (0.5,))\n",
    "    ])\n",
    "    # 训练参数\n",
    "    device: str = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
    "    batch_size: int = 2048\n",
    "    num_workers: int = 4\n",
    "    lr: float = 1e-4\n",
    "    num_epochs: int = 500\n",
    "    # KL Annealing\n",
    "    kl_anneal_epochs: int = 100 # 在多少个epoch内将beta从0增加到1\n",
    "    kl_anneal_start_epoch: int = 10 # 从第几个epoch开始退火\n",
    "    # 模型参数\n",
    "    img_size = 28\n",
    "    latent_dim1 = 32\n",
    "    latent_dim2 = 32\n",
    "\n",
    "g_opt = Config()\n",
    "os.makedirs(g_opt.save_dir, exist_ok=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "36bf4028",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "共有60000张训练图片，10000张测试图片\n"
     ]
    }
   ],
   "source": [
    "# 数据集\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "train_dataset = tv.datasets.MNIST(\n",
    "    root=g_opt.data_path,\n",
    "    train=True,\n",
    "    transform=g_opt.transform,\n",
    "    download=True\n",
    ")\n",
    "test_dataset = tv.datasets.MNIST(\n",
    "    root=g_opt.data_path,\n",
    "    train=False,\n",
    "    transform=g_opt.transform,\n",
    "    download=True\n",
    ")\n",
    "print(f\"共有{len(train_dataset)}张训练图片，{len(test_dataset)}张测试图片\")\n",
    "# 数据加载器\n",
    "train_loader = DataLoader(\n",
    "    train_dataset,\n",
    "    batch_size=g_opt.batch_size,\n",
    "    shuffle=True,\n",
    "    num_workers=g_opt.num_workers,\n",
    "    pin_memory=True\n",
    ")\n",
    "val_loader = DataLoader(\n",
    "    test_dataset,\n",
    "    batch_size=g_opt.batch_size,\n",
    "    shuffle=False,\n",
    "    num_workers=g_opt.num_workers,\n",
    "    pin_memory=True,\n",
    ")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "a6c52e13",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Utility\n",
    "def save_images(tensor, fn, nrow=8, padding=2):\n",
    "    \"\"\"\n",
    "    保存图像\n",
    "    \"\"\"\n",
    "    grid = tv.utils.make_grid(tensor, nrow=nrow, padding=padding)\n",
    "    tv.utils.save_image(grid, fn)\n",
    "\n",
    "def kl_divergence_gaussians(q_mu, q_logvar, p_mu, p_logvar):\n",
    "    \"\"\"\n",
    "    计算两个多元高斯分布之间的KL散度 D_KL(q || p)\n",
    "    q: N(q_mu, exp(q_logvar))\n",
    "    p: N(p_mu, exp(p_logvar))\n",
    "    \"\"\"\n",
    "    q_std = torch.exp(0.5 * q_logvar)\n",
    "    p_std = torch.exp(0.5 * p_logvar)\n",
    "    \n",
    "    # D_KL(q || p) = 0.5 * [ log(det(Sigma_p)/det(Sigma_q)) - k + tr(Sigma_p^-1 * Sigma_q) + (mu_p - mu_q)^T Sigma_p^-1 (mu_p - mu_q) ]\n",
    "    # 对于对角协方差矩阵，公式简化为：\n",
    "    kl_div = torch.sum(p_logvar - q_logvar - 1 + (q_std.pow(2) + (q_mu - p_mu).pow(2)) / p_std.pow(2), dim=1)\n",
    "    \n",
    "    return 0.5 * kl_div\n",
    "\n",
    "def hvae_loss(x, x_recon, mu_q1, logvar_q1, mu_q2, logvar_q2, mu_p1, logvar_p1, beta: float = 1.0):\n",
    "    # 1. 重构损失\n",
    "    recon_loss = F.binary_cross_entropy(x_recon.view(-1, 784), x.view(-1, 784), reduction='sum')\n",
    "    # 2. KL 散度 for z2\n",
    "    # D_KL(q(z2|z1) || p(z2)), 其中 p(z2) ~ N(0, I)\n",
    "    mu_p2 = torch.zeros_like(mu_q2)\n",
    "    logvar_p2 = torch.zeros_like(logvar_q2)\n",
    "    kl2 = kl_divergence_gaussians(mu_q2, logvar_q2, mu_p2, logvar_p2).sum()\n",
    "    # 3. KL 散度 for z1\n",
    "    # D_KL(q(z1|x) || p(z1|z2))\n",
    "    kl1 = kl_divergence_gaussians(mu_q1, logvar_q1, mu_p1, logvar_p1).sum()\n",
    "    \n",
    "    # 计算总KL散度\n",
    "    kld_loss = kl1 + kl2\n",
    "    \n",
    "    # 应用 KL 退火\n",
    "    total_loss = recon_loss + beta * kld_loss\n",
    "\n",
    "    # 归一化\n",
    "    batch_size = x.size(0)\n",
    "    total_loss /= batch_size\n",
    "    recon_loss /= batch_size\n",
    "    kld_loss /= batch_size\n",
    "\n",
    "    return total_loss, recon_loss, kld_loss\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "81cbece5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# MHVAE Model\n",
    "from typing import Tuple\n",
    "\n",
    "\n",
    "class MHVAE(nn.Module):\n",
    "    def __init__(self, img_size=28, latent_dim1=20, latent_dim2=10):\n",
    "        super(MHVAE, self).__init__()\n",
    "        self.img_size = img_size\n",
    "        self.latent_dim1 = latent_dim1\n",
    "        self.latent_dim2 = latent_dim2\n",
    "        self.encoder_out_dim = 64 * 4 * 4 # 1024\n",
    "        \n",
    "        # --- 1. 推断模型 (Inference Model: q(z1|x), q(z2|z1,x)) ---\n",
    "        \n",
    "        # 1a. 从 x 提取特征\n",
    "        self.encoder_base = nn.Sequential(\n",
    "            nn.Conv2d(1, 16, kernel_size=4, stride=2, padding=1), # (B, 1, 28, 28) -> (B, 16, 14, 14)\n",
    "            nn.ReLU(),\n",
    "            nn.Conv2d(16, 32, kernel_size=4, stride=2, padding=1), # (B, 16, 14, 14) -> (B, 32, 7, 7)\n",
    "            nn.ReLU(),\n",
    "            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1), # (B, 32, 7, 7) -> (B, 64, 4, 4)\n",
    "            nn.ReLU(),\n",
    "            nn.Flatten() # (B, 1024)\n",
    "        )\n",
    "        \n",
    "        # 1b. 从特征推断 z1: q(z1|x)\n",
    "        self.fc_q1_mu = nn.Linear(self.encoder_out_dim, latent_dim1)\n",
    "        self.fc_q1_logvar = nn.Linear(self.encoder_out_dim, latent_dim1)\n",
    "        \n",
    "        # 1c. 从 z1 和 x 的特征推断 z2: q(z2|z1,x)\n",
    "        self.encoder_z2 = nn.Sequential(\n",
    "            nn.Linear(latent_dim1 + self.encoder_out_dim, 128),\n",
    "            nn.ReLU()\n",
    "        )\n",
    "        self.fc_q2_mu = nn.Linear(128, latent_dim2)\n",
    "        self.fc_q2_logvar = nn.Linear(128, latent_dim2)\n",
    "        # --- 2. 生成模型 (Generative Model: p(z1|z2), p(x|z1)) ---\n",
    "        # 2a. 从 z2 生成 z1 的先验分布: p(z1|z2)\n",
    "        self.decoder_p1 = nn.Sequential(\n",
    "            nn.Linear(latent_dim2, 128),\n",
    "            nn.ReLU()\n",
    "        )\n",
    "        self.fc_p1_mu = nn.Linear(128, latent_dim1)\n",
    "        self.fc_p1_logvar = nn.Linear(128, latent_dim1)\n",
    "        \n",
    "        # 2b. 从 z1 重构 x: p(x|z1)\n",
    "        self.decoder_input = nn.Linear(latent_dim1, self.encoder_out_dim)\n",
    "        self.decoder_x = nn.Sequential(\n",
    "            nn.Unflatten(1, (64, 4, 4)),\n",
    "            nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1), # (B, 64, 4, 4) -> (B, 32, 7, 7)\n",
    "            nn.ReLU(),\n",
    "            nn.ConvTranspose2d(32, 16, kernel_size=4, stride=2, padding=1), # (B, 32, 7, 7) -> (B, 16, 14, 14)\n",
    "            nn.ReLU(),\n",
    "            nn.ConvTranspose2d(16, 1, kernel_size=4, stride=2, padding=1), # (B, 16, 14, 14) -> (B, 1, 28, 28)\n",
    "            nn.Sigmoid(),\n",
    "        )\n",
    "    def reparameterize(self, mu, logvar):\n",
    "        std = torch.exp(0.5 * logvar)\n",
    "        eps = torch.randn_like(std)\n",
    "        return mu + eps * std\n",
    "    def encode(self, x: torch.Tensor) -> Tuple[torch.Tensor, ...]:\n",
    "        # 推断 q(z1|x) 和 q(z2|z1,x)\n",
    "        h = self.encoder_base(x)\n",
    "        \n",
    "        # q(z1|x)\n",
    "        mu_q1 = self.fc_q1_mu(h)\n",
    "        logvar_q1 = self.fc_q1_logvar(h)\n",
    "        z1 = self.reparameterize(mu_q1, logvar_q1)\n",
    "        \n",
    "        # q(z2|z1,x)\n",
    "        h_for_z2 = torch.cat((h, z1), dim=1)\n",
    "        h2 = self.encoder_z2(h_for_z2)\n",
    "        mu_q2 = self.fc_q2_mu(h2)\n",
    "        logvar_q2 = self.fc_q2_logvar(h2)\n",
    "        z2 = self.reparameterize(mu_q2, logvar_q2)\n",
    "        \n",
    "        return z1, mu_q1, logvar_q1, z2, mu_q2, logvar_q2\n",
    "    def decode(self, z1: torch.Tensor, z2: torch.Tensor) -> Tuple[torch.Tensor, ...]:\n",
    "        # 生成 p(z1|z2) 和 p(x|z1)\n",
    "        \n",
    "        # p(z1|z2)\n",
    "        h_p1 = self.decoder_p1(z2)\n",
    "        mu_p1 = self.fc_p1_mu(h_p1)\n",
    "        logvar_p1 = self.fc_p1_logvar(h_p1)\n",
    "        \n",
    "        # p(x|z1)\n",
    "        h_x = self.decoder_input(z1)\n",
    "        x_recon = self.decoder_x(h_x)\n",
    "        \n",
    "        return x_recon, mu_p1, logvar_p1\n",
    "    def forward(self, x: torch.Tensor):\n",
    "        # 推断路径\n",
    "        z1, mu_q1, logvar_q1, z2, mu_q2, logvar_q2 = self.encode(x)\n",
    "        \n",
    "        # 生成路径\n",
    "        x_recon, mu_p1, logvar_p1 = self.decode(z1, z2)\n",
    "        \n",
    "        return x_recon, mu_q1, logvar_q1, mu_q2, logvar_q2, mu_p1, logvar_p1\n",
    "        \n",
    "    def sample(self, num_samples: int, device: torch.device):\n",
    "        \"\"\"从先验中采样生成新图像\"\"\"\n",
    "        # 从顶层先验 p(z2) ~ N(0, I) 采样\n",
    "        z2 = torch.randn(num_samples, self.latent_dim2).to(device)\n",
    "        \n",
    "        # 基于 z2 计算 p(z1|z2) 的参数\n",
    "        h_p1 = self.decoder_p1(z2)\n",
    "        mu_p1 = self.fc_p1_mu(h_p1)\n",
    "        logvar_p1 = self.fc_p1_logvar(h_p1)\n",
    "        \n",
    "        # 从 p(z1|z2) 采样 z1\n",
    "        z1 = self.reparameterize(mu_p1, logvar_p1)\n",
    "        \n",
    "        # 从 z1 生成图像\n",
    "        h_x = self.decoder_input(z1)\n",
    "        generated_x = self.decoder_x(h_x)\n",
    "        \n",
    "        return generated_x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "21652e20",
   "metadata": {},
   "outputs": [],
   "source": [
    "from tqdm import tqdm\n",
    "\n",
    "def train_epoch(model, train_loader, optimizer, device, beta):\n",
    "    model.train()\n",
    "    train_loss_acc, recon_loss_acc, kld_loss_acc = 0, 0, 0\n",
    "    for x, y in train_loader:\n",
    "        x = x.to(device)\n",
    "        x_recon, mu_q1, logvar_q1, mu_q2, logvar_q2, mu_p1, logvar_p1 = model(x)\n",
    "        loss, recon_loss, kld_loss = hvae_loss(x, x_recon, mu_q1, logvar_q1, mu_q2, logvar_q2, mu_p1, logvar_p1, beta)\n",
    "        optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "        train_loss_acc += loss.item()\n",
    "        recon_loss_acc += recon_loss.item()\n",
    "        kld_loss_acc += kld_loss.item()\n",
    "        \n",
    "    return train_loss_acc/len(train_loader), recon_loss_acc/len(train_loader), kld_loss_acc/len(train_loader)\n",
    "\n",
    "def val_epoch(model, val_loader, device, beta):\n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        val_loss_acc, recon_loss_acc, kld_loss_acc = 0, 0, 0\n",
    "        for x, y in val_loader:\n",
    "            x = x.to(device)\n",
    "            x_recon, mu_q1, logvar_q1, mu_q2, logvar_q2, mu_p1, logvar_p1 = model(x)\n",
    "            loss, recon_loss, kld_loss = hvae_loss(x, x_recon, mu_q1, logvar_q1, mu_q2, logvar_q2, mu_p1, logvar_p1, beta)\n",
    "            val_loss_acc += loss.item()\n",
    "            recon_loss_acc += recon_loss.item()\n",
    "            kld_loss_acc += kld_loss.item()\n",
    "            \n",
    "    return val_loss_acc/len(val_loader), recon_loss_acc/len(val_loader), kld_loss_acc/len(val_loader)\n",
    "\n",
    "def train(model, train_loader, val_loader, optimizer, device, num_epochs):\n",
    "    model.to(device)\n",
    "    best_loss = float('inf')\n",
    "\n",
    "    epoch_pbar = tqdm(range(num_epochs), desc=\"Epochs\", position=0)\n",
    "\n",
    "    for epoch in epoch_pbar:\n",
    "        # 计算当前 beta 值 for KL annealing\n",
    "        if epoch < g_opt.kl_anneal_start_epoch:\n",
    "            beta = 0.0\n",
    "        else:\n",
    "            anneal_progress = (epoch - g_opt.kl_anneal_start_epoch) / g_opt.kl_anneal_epochs\n",
    "            beta = min(1.0, anneal_progress)\n",
    "\n",
    "        train_loss, _, _ = train_epoch(model, train_loader, optimizer, device, beta)\n",
    "        val_loss, recon_loss, kld_loss = val_epoch(model, val_loader, device, beta)\n",
    "        epoch_pbar.set_postfix(\n",
    "            Train=f\"{train_loss:.4f}\",\n",
    "            Test=f\"{val_loss:.4f}\",\n",
    "            Recon=f\"{recon_loss:.3f}\",\n",
    "            KLD=f\"{kld_loss:.3f}\",\n",
    "            beta=f\"{beta:.3f}\"\n",
    "        )\n",
    "        # print(f\"Epoch {epoch+1}/{num_epochs} TrainLoss: {train_loss:.4f} TestLoss: {val_loss:.4f} ReconLoss: {recon_loss:.3f} KLD: {kld_loss:.3f}\")        \n",
    "        cur_val_loss = val_loss\n",
    "        # 保存最佳模型\n",
    "        if cur_val_loss < best_loss:\n",
    "            best_loss = cur_val_loss\n",
    "            torch.save({\n",
    "                'epoch': epoch,\n",
    "                'model_state_dict': model.state_dict(),\n",
    "                'optimizer_state_dict': optimizer.state_dict(),\n",
    "                'loss': best_loss\n",
    "            }, f\"{g_opt.save_dir}/best.pth\")\n",
    "            \n",
    "        torch.save({\n",
    "            'epoch': epoch,\n",
    "            'model_state_dict': model.state_dict(),\n",
    "            'optimizer_state_dict': optimizer.state_dict(),\n",
    "            'loss': cur_val_loss\n",
    "        }, f\"{g_opt.save_dir}/last.pth\")\n",
    "\n",
    "        # if epoch % g_opt.save_every == 0 or epoch == num_epochs - 1 or epoch == 0:\n",
    "        #     save_epoch(model, val_loader, epoch, device, times=1)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "f3db9372",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Epochs: 100%|██████████| 500/500 [05:23<00:00,  1.54it/s, KLD=25.683, Recon=74.611, Test=100.2936, Train=100.8337, beta=1.000]  \n"
     ]
    }
   ],
   "source": [
    "model = MHVAE(img_size=g_opt.img_size, latent_dim1=g_opt.latent_dim1, latent_dim2=g_opt.latent_dim2)\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=g_opt.lr)\n",
    "train(model, train_loader, val_loader, optimizer, g_opt.device, g_opt.num_epochs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "e270b081",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "测试集图像重建效果：\n"
     ]
    },
    {
     "data": {
      "image/png": "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bAEAAAAArsbEFgAAAADgakxsAQAAAACulqY1tnp7HxGRu+66S8Xbt283cuXLl1exXmsgYtYb1KxZ08gdPHhQxZ4uh227evWqsX3ixAkV661rbAcOHDC2qbFNTK+Pux0DBw5UcZkyZTzed8OGDTeM4S4vvviisa2/lzjWUs9XX32lYrsVT0rZLQji4+NVXLx4cSOnt434+eefjVymTJm8Mp6Mwm6hpbdk2bNnj5EbO3ZsqozputatW6fq8+HmKlasaGxXq1Ytyfvav6O+/vprn4zJH+XPn9/YbteuXZL37d69u4r136qpRa+r/fbbb5O8n11jq19fBTc2YMAAFettnW6FfR2KBx54wNjW2wbZ9biXL19O0XOmJc7YAgAAAABcjYktAAAAAMDV0nQp8nfffedxW7d8+fIkc3pLgMqVKxs5vfXD3XffneyxXbx40djeuXOniu1l0vryAHvpFrynRYsWxrZ+WfusWbMauePHjxvbL7/8sorPnz/vg9HBF+wWYNWrVze29eOSdh++U79+fWO7bNmyKrbb+yS33c/06dON7RUrVhjbZ86cUXGjRo2MnKcWI0899ZSKp02blqyxZGSvvvqqsa230LKXrOnLw31F/z6133feaiWFlPO0JNZmH9NIvokTJxrbTzzxhIrtlmbz589PlTElpW7duiouUKCAkZs1a5aK58yZk1pDci277KZr165J3nfLli0qPnbsmJFr3Lhxkn8XGhpqbOvLnT/55BMjd/To0aQHm05xxhYAAAAA4GpMbAEAAAAArsbEFgAAAADgamlaY+stp06dUvGqVauSvJ+nGt6b0etK9JpeEZE//vhDxfPmzUvxc8Azu77SrqvV2fth9erVPhkTfMuusbOlRWuDjEKvb/7ss8+MXFhYWLIew27ttWDBAhWPGDHCyHmqfbcfp1evXioODw83chMmTFBxtmzZjNw777yj4itXriT5fP6uffv2Km7WrJmR2717t4rTooWWXj9t19T+8MMPKj59+nQqjQi6evXqeczr7UE81cLDM8dxjG39WDh8+LCRS42WLNmzZ1fxK6+8YuSefvppFdvj7tatm28H5mfs6wSFhISo+McffzRy+u8j+7vuscceU7G9vyIiIoztggULqviLL74wcg8++KCKY2NjPQ093eCMLQAAAADA1ZjYAgAAAABczS+WIvtC/vz5je2p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hg3y/fffy/333+9xDOfPn5f69evLP//8I71795Y77rhD1q1bJy+//LIcOXJEJk2aJCIijuNI69atZe3atdKnTx8pX768LFq0SDp37nzL47U98sgjUr58eXnttddk2bJlMnr0aMmbN6/MmDFDGjVqJOPHj5dPPvlEBgwYIHfffbfUq1dPRETOnj0r77//vjz22GPSs2dPiYuLkw8++ECaNm0qP//8s1SuXFlERBISEqRly5by888/y1NPPSXlypWTL7744oZjT+77bfjw4TJu3Djp0aOH1KhRQ86ePSsbN26UX3/9VZo0aXLbrwkA+AUHAAA/NnPmTEdEnG+//dY5ceKEc/DgQSc6OtoJDw93goKCnIMHD6r73nfffU7FihWdixcvqtsSEhKcWrVqOaVLl1a3DR061BERZ+HChYmeLyEhwXEcx5k0aZIjIs6cOXNU7vLly869997r5MyZ0zl79qzjOI6zd+9eR0ScfPnyObGxseq+X3zxhSMiztKlSx3HcZxTp045IuK8/vrrHv+9d955p1O/fv0kX4c6deo4V69eNXKdO3d2ihcvnuhvhg0b5ug/FXbt2uUEBgY6bdu2da5du3bDf7enMYwaNcoJDg52du7cadw+aNAgJ1OmTM6BAwccx3GcxYsXOyLiTJgwQd3n6tWrTt26dR0RcWbOnJnUP99xHMdZtWqVIyLO/PnzE/1bevXqZTxm0aJFnYCAAOe1115Tt586dcrJnj2707lzZ+O+ly5dMp7n1KlTToECBZxu3bqp2xYsWOCIiDNp0iR127Vr15xGjRolGnty32+VKlVymjdv7vHfDAAZHUuRAQAZQuPGjSU8PFyKFSsm7du3l+DgYFmyZIlajhsbGyvff/+9PPzwwxIXFycxMTESExMjJ0+elKZNm8quXbvUVZQXLFgglSpVUmfUdNeX7n711VdSsGBBeeyxx1QuS5Ys8uyzz0p8fLysXr3a+LtHHnnEOHt8fan033//LSIi2bNnl6xZs8oPP/wgp06dSvHr0LNnT8mUKVOK/nbx4sWSkJAgQ4cOlcBA8yfEjZYs2+bPny9169aVPHnyqNc3JiZGGjduLNeuXZM1a9aIyL+vXebMmeWpp55Sf5spUybp169fisat69Gjh/GY1atXF8dxpHv37ur23LlzS9myZdVrf/2+WbNmFZF/z8rGxsbK1atXpXr16vLrr7+q+y1fvlyyZMkiPXv2VLcFBgbKM888Y4zjVt5vuXPnlq1bt8quXbtu+98PAP6KpcgAgAxhypQpUqZMGTlz5ox8+OGHsmbNGgkKClL53bt3i+M4MmTIEBkyZMgNH+P48eNSpEgR2bNnj7Rr187j8+3fv19Kly6daAJYvnx5ldfdcccdxvb1Se71SWxQUJCMHz9e+vfvLwUKFJCaNWtKixYt5Mknn5SCBQsm4xX4V8mSJZN9X9uePXskMDBQoqKiUvT3u3btki1btkh4ePgN88ePHxeRf1+bQoUKSc6cOY182bJlU/S8Ovt1Dg0NlWzZsklYWFii20+ePGncNnv2bJk4caLs2LFDrly5om7XX9PrY8+RI4fxt5GRkcb2rbzfRo4cKa1bt5YyZcpIhQoV5IEHHpBOnTrJXXfdlfx/OAD4OSa2AIAMoUaNGuqqyG3atJE6derI448/Ln/99ZfkzJlTXbxowIAB0rRp0xs+hj058aakzqI6jqPi559/Xlq2bCmLFy+Wb775RoYMGSLjxo2T77//XqpUqZKs58mePXui25I623rt2rVkPWZyJSQkSJMmTeTFF1+8Yb5MmTJefb4budHrnJzXfs6cOdKlSxdp06aNDBw4UPLnzy+ZMmWScePGJboAWXLcyvutXr16smfPHvniiy9kxYoV8v7778tbb70l06dPN85AA0BGxsQWAJDhXJ+QNGzYUN555x0ZNGiQlCpVSkT+XS7cuHFjj38fEREhf/75p8f7FC9eXLZs2SIJCQnGWdsdO3aofEpERERI//79pX///rJr1y6pXLmyTJw4UebMmSMiyVsSbMuTJ88Nrzhsn1WOiIiQhIQE2bZtm7pY0o0kNYaIiAiJj4+/6etbvHhx+e677yQ+Pt44a/vXX395/Dtfio6OllKlSsnChQuNf9+wYcOM+xUvXlxWrVol58+fN87a7t6927jfrbzfRETy5s0rXbt2la5du0p8fLzUq1dPhg8fzsQWAP4/amwBABlSgwYNpEaNGjJp0iS5ePGi5M+fXxo0aCAzZsyQI0eOJLr/iRMnVNyuXTvZvHmzLFq0KNH9rp/la9asmRw9elTmzZunclevXpXJkydLzpw5pX79+rc03vPnz8vFixeN2yIiIiQkJEQuXbqkbgsODr7ltjgRERFy5swZ2bJli7rtyJEjif59bdq0kcDAQBk5cmSi9jz62c2kxvDwww/L+vXr5ZtvvkmUO336tFy9elVE/n3trl69KtOmTVP5a9euyeTJk2/p3+VN18/q6v/ODRs2yPr16437NW3aVK5cuSLvvfeeui0hIUGmTJli3O9W3m/2kuicOXNKZGSksd8BIKPjjC0AIMMaOHCgdOjQQWbNmiV9+vSRKVOmSJ06daRixYrSs2dPKVWqlBw7dkzWr18vhw4dks2bN6u/i46Olg4dOki3bt2kWrVqEhsbK0uWLJHp06dLpUqVpFevXjJjxgzp0qWLbNq0SUqUKCHR0dHy3//+VyZNmiQhISG3NNadO3fKfffdJw8//LBERUVJ5syZZdGiRXLs2DF59NFH1f2qVasm06ZNk9GjR0tkZKTkz59fGjVq5PGxH330UXnppZekbdu28uyzz8r58+dl2rRpUqZMGePCSJGRkTJ48GAZNWqU1K1bVx566CEJCgqSX375RQoXLizjxo3zOIaBAwfKkiVLpEWLFtKlSxepVq2anDt3Tv744w+Jjo6Wffv2SVhYmLRs2VJq164tgwYNkn379klUVJQsXLhQzpw5c0uvmTe1aNFCFi5cKG3btpXmzZvL3r17Zfr06RIVFSXx8fHqfm3atJEaNWpI//79Zffu3VKuXDlZsmSJxMbGioh5Nju577eoqChp0KCBVKtWTfLmzSsbN26U6Oho6du3b+q+CACQnqXdBZkBAPC9621ufvnll0S5a9euOREREU5ERIRqgbNnzx7nySefdAoWLOhkyZLFKVKkiNOiRQsnOjra+NuTJ086ffv2dYoUKeJkzZrVKVq0qNO5c2cnJiZG3efYsWNO165dnbCwMCdr1qxOxYoVE7Wqud7u50ZtfETEGTZsmOM4jhMTE+M888wzTrly5Zzg4GAnNDTUueeee5zPP//c+JujR486zZs3d0JCQhwRUW13PL0OjuM4K1ascCpUqOBkzZrVKVu2rDNnzpxE7X6u+/DDD50qVao4QUFBTp48eZz69es7K1euvOkYHMdx4uLinJdfftmJjIx0smbN6oSFhTm1atVy3njjDefy5cvG69upUycnV65cTmhoqNOpUyfnt99+u+12PydOnDDu27lzZyc4ODjRY9SvX9+588471XZCQoIzduxYp3jx4k5QUJBTpUoV58svv7xhq6QTJ044jz/+uBMSEuKEhoY6Xbp0cf773/86IuJ89tlnxn2T834bPXq0U6NGDSd37txO9uzZnXLlyjljxowxXi8AyOgCHEdbUwMAAACvW7x4sbRt21bWrl0rtWvXTuvhAIDfYWILAADgRRcuXDCuPn3t2jW5//77ZePGjXL06NEbXpkaAHB7qLEFAADwon79+smFCxfk3nvvlUuXLsnChQtl3bp1MnbsWCa1AOAjnLEFAADwok8//VQmTpwou3fvlosXL0pkZKQ89dRTXOwJAHyIiS0AAAAAwNXoYwsAAAAAcDUmtgAAAAAAV0vWxaMSEhLk8OHDEhISYjQWR+pzHEfi4uKkcOHCEhh4+/9fgn2bfnhz37Jf0w+OWf/FvvVf7Fv/xXetf+KY9V+3sm+TNbE9fPiwFCtWzCuDg3ccPHhQihYtetuPw75Nf7yxb9mv6Q/HrP9i3/ov9q3/4rvWP3HM+q/k7Ntk/S+NkJAQrwwI3uOtfcK+TX+8sU/Yr+kPx6z/Yt/6L/at/+K71j9xzPqv5OyTZE1sOQWf/nhrn7Bv0x9v7BP2a/rDMeu/2Lf+i33rv/iu9U8cs/4rOfuEi0cBAAAAAFyNiS0AAAAAwNWY2AIAAAAAXI2JLQAAAADA1ZLV7gdIj/Qicsdx0nAkAAAAANISZ2wBAAAAAK7GxBYAAAAA4GpMbAEAAAAArkaNLVKd3WA5KChIxQUKFDByXbp0UXH16tWNXHh4uIrPnz9v5D799FNj++OPP1bxpUuXbm3AcIUsWbIY29mzZ1ex/f64evVqqowpowsMDExyOyEhwcjZ2wAAALeCM7YAAAAAAFdjYgsAAAAAcDWWIiNVZM2aVcWlS5c2coMGDVJx69atjVzOnDlVbC9h1pcu2rny5csb2/Hx8SpesGCBkbty5YrHsSP9ypMnj4pnzZpl5HLlyqXivn37GrmtW7f6dFwZmb4k3D4On3/+eRUXL17cyE2bNs3YXrp0qYopH0gbmTJlMrZz5MihYrvFmr7c/2bt1/Ql6fZz6Nv24+if+fbnNi3f0ob93at/JuufwSIix44dU/GFCxd8O7AMyt4fHBfIaDhjCwAAAABwNSa2AAAAAABXY2ILAAAAAHA1amzhE3oLHxGzVc/w4cONXM2aNVWcLVs2I3ft2jUVnzp1ysjpNV16La5I4tYhHTp0UPGyZcuMHDW27mHXD+nvq/r16xu5AwcOqPjw4cO+HVgGZu8TvQ1XmzZtjFzLli1VbNff2TW3+/btU/GmTZuMHHVjvqPXv9o10vrn6K5du4zckiVLVHzx4kUjp3+Oi5jvGbvGVv/uKFOmjJHLly+fin/66Scjd/r0aRXz/vAd+3i/8847je2ZM2equEiRIkZOvw7CiBEjjBx19Mlnt7bTr00SERFh5D766CMV6zXOIr5pseapnWOJEiWMnP45YX9H67/LOJ5vzm6tp79mGe3144wtAAAAAMDVmNgCAAAAAFzN50uR7WVGnng6XZ6RT6u7kb1URl9uXKpUKSMXFxenYnt5m75s+MsvvzRy+pIXfYmciEi7du2M7YoVK6rYXt7266+/qpj3Vvpmv6969Oih4uzZsxu5nTt3qvjs2bO+HVgGZu+TSpUqqdheihwaGqpi+7uhUKFCxrZ+DP/5559Gzl7qCu/R99GYMWOMXNmyZVVsLyXVlxtfvXrVyHla8mjfV3+c/PnzG7lWrVqpeP/+/UZOX4oM37GP9wcffNDY1r9f7ZIk/f1jL52EZ5kz/+/n+t13323k9GPx8uXLRm7Dhg0qjomJMXLe+l2t/xbTxykiUrlyZRUPHjzYyP38888qnjhxopGz/x1IvMxbL+fRf2OLmC0NDx065NuBpTN8sgAAAAAAXI2JLQAAAADA1ZjYAgAAAABczSs1tp4u7x0SEmLk9EuR161b18jplwK3W0HobWB27Nhh5PT6udjYWCOnr9PXazlFRM6cOWNsX7hwQcV2vZ6eO3r0qJE7cuSIirlk/b/sFjp//PGHir/99tskc1999ZWRO3HihIrtujq9lqNcuXJGrlu3bkmOzX5vUVfrHva+0+tK7JpNvR2I3W4Et0evj4uMjDRyTz31lIrt1hP6PrK/N+zarIceekjFmzdvNnKff/65in3RsiIjsesg+/Tpo+ImTZoYOb1diF4fJ2J+R97OPtGP1apVqxq52rVrq/jDDz80cnyOpw67JV+1atU85nUHDx5UMW32bk2ePHlU3L179yRzS5cuNXJ//fWXiu16dm8dM/r3gd22bfTo0SqOiooycvpniD02/Ev/ngwLCzNy77//voobNWpk5PR5Svv27Y3cli1bjG1/++zkjC0AAAAAwNWY2AIAAAAAXM0n7X70pUT28kB9yah9mXh92Vru3LmNXNasWVVst5BI7mXj7aVvnpZL2Tn9b+3WIWvXrlWxvQT25MmTyRqbv7Ev1a6/Rps2bTJy+hI2e7mxpyUS+j4pWrSokbOXkuv7k9Yv7mEfs3abA31pzvnz543cvHnzfDewDE5fbqgvPRYRqVWrloqDg4OTfAx7ebj9mRseHq7iZ555xsjpS6m2bduWjBFDpx9X9tLBQYMGqdhu7bJo0SIV2+12vLUkXF+S3qBBAyOnlyLYrUuQOuzvVrt9nv7e0r/bRcz3D+UhntnfffoyXn1JvojIuXPnVDxz5kwjpx8nvlpymiNHDhUPGzbMyN11110qtksFv/jiCxWzNP3G9M/gl156ycg98MADKtbnSCIiJUuWVPHQoUONXL9+/Yztw4cP3/Y40xPO2AIAAAAAXI2JLQAAAADA1ZjYAgAAAABczSs1tva6ff2y3faa+hUrViSZK1KkiIrLli1r5PT6grx58xo5vRbLvmS4vu7cbj1kr+nXt+1aT/057Ppf/TLb9rjXrVsnGZH9ntDrH+3XVq8lsf9O37ZrTnLmzKli+3Lmdr3BP//8o2K7Ngzpl10/P2DAAGNbr/XctWuXkdNbk+D22NdKaNWqlYofffRRIxcaGprk4+ifA3b9nV3PqddaVq5c2cjpbQ66du1q5PT2Frgx/bix6630Gsq9e/cauXHjxqnYV+059O/XKlWqGDn9PcPxnTYKFSpkbNvtvvTPCr0VoojIxo0bVexvLUa8zW6b1KNHDxXnz5/fyH399dcqtlu5+KJ21f5e1tvu3X///UZOP2ZnzJhh5Hbu3Kli3g//sr9rGzZsqOJOnToZOf07Mz4+3sidPn1axXfccYeRs/fDCy+8oOLdu3ff2oDTIc7YAgAAAABcjYktAAAAAMDVfNLuR19SYLd90S8rvXTpUiOnLzW1lzroOftUvX5fTzm9VYCIyKVLl4xtfembvZRVv1z2k08+KUnx1fIsf2K3hUhuuyZ7qeLzzz+v4goVKhg5e/nNmDFjVJxRWzC5kd0upnz58sa23jJi1qxZRs7+7EHK6aUYIiKDBw9WcZ48eZL8O305lIjZ7sNeOm4fw/fee6+KCxYsaOT0JarR0dFGrl27dirWl7rhf/Q2WXZLHX25+KhRo4ycXT7kC3obQHs55ubNm1Vst/eC7+i/v5o0aWLk7H2k//7T2/yJsM9uRn+dK1WqZOTq16+vYvt37qpVq1Rs/671BfszX/99bP9O09s7LliwwMilxljdxi7l0ZcJ23OYo0ePqth+bfXParu1at26dY1t/XvZztnf4W7AGVsAAAAAgKsxsQUAAAAAuBoTWwAAAACAq/mkxja5PLV2seswdSm9fPmZM2c85vX6BrvGVl/bbred0deg02ri1un73a631WtJ7rvvPiPXt2/fJP/Orq3T6y+5rLx7lClTxti2a27PnTun4sWLF6fGkDIM/dh75JFHjJze4sOu99LbO3zwwQdG7t1331WxXbtjf66WKlVKxW+88YaRq1ix4g3vJyIyd+5cFT/wwANG7sSJE5IR2a+t3j7JbgWhH1Nr1qwxcnpNu7fo17YQMdsP2e8tvRbMF2PBjel1k3Y7F3sf6XWT9nUP+O71TP9+s1u75MuXT8X2tVyqVaum4vnz5xu5uLg4FXv6XW1/Rtj7Sv9N3LFjRyOnf0/bdfj6e0CvCb3Rc2RU+mtfunRpI6e3ENX3pYjI22+/reLZs2cbOb1tm10TbV+rpFixYiq263H171O34IwtAAAAAMDVmNgCAAAAAFwtTZcipzf6sgj7stq1atW64f1EzCWQ9lIB3Jy+DCN37txGrlmzZiq2W0/oyytOnTpl5B566CFj++LFi7c7TKQSfWnbkCFDjFzOnDmN7QMHDqh4//79vh1YBqMfXz169DByQUFBKraXt+ktPuxjVl/marM/V/UlbR06dDBy+hKsNm3aGLmoqCgV2+8fvUWYp2V5/sZeLqp/Pur7UkTk999/V7G9dNAX7HKDcuXKqdhuB7Jy5UqfjweJ6d/L+jJ2kcRLWPXv4i1btvhyWK5nv3b6cl/7ddaXg9tLkfXPvN69exs5/fN49+7dRi4+Pl7FN1vary9ffeaZZ5Icm956SERk9erVSY4b/9JL6ezSGr1UY8OGDUZOX3Zu/wbW2Z/xdvmH3rLLbuf1+eefq9gt5R+csQUAAAAAuBoTWwAAAACAqzGxBQAAAAC4GjW2Gr3e4cUXXzRyBQoUUPHBgweN3NixY1Wckeq2vEVf71+nTh0jN2bMGBUXLlzYyOn1GgMHDjRyO3bs8OYQkYoiIiJU3KhRIyNn13jol7injtq7ChYsqOKiRYsaOb0e9vDhw0ZOb8Ol13DdKv2zNCYmxsh9//33KrZrbPU6tcaNGyeZy0jvF7t9nd5Cwn4doqOjVZzS1no3o9d0vfbaa0ZOb1Nh180fOnTIJ+OBya791Fvt2dfBsD+Tt27dquKMdIx5w/nz51X81VdfGTm99lJvqSZi7pOuXbsauaefflrFds38vHnzVPzHH38YubNnzxrb06dPv+FYRMzrIWzevNnI2XXySEw/3vLmzWvk9NrZn3/+2cjp7wP9M1XE/B1lX8fApn+f6638RETCwsJUfOzYMY+Pk15wxhYAAAAA4GpMbAEAAAAArsZSZI2+1NW+nLl+qv7ll182cm45PZ9e2MuccuTIoeInn3zSyOnLIW36kpfPPvvMS6NDarNbkfTp00fFdnuf06dPG9tz585Vsd0uBrdGbzkgIlKlShUV2+0B9LZmzz77rJGzW0p4g71v9SVYdvmH/vliv3/05VoZaZmk/frpy8fDw8ON3M6dO1VsvyeSW2pjv19CQ0ON7QceeEDFNWvWTPJx7KXIvloaDZO+HFxE5IUXXlCxvW/t40hfQktplmf2cam/lnpLMxGRTZs2qVhvryNiLl9t2bKlkbvzzjtVbO/Xe++9V8WlS5c2csWKFTO2K1asqGL7N5z+mb9x40Yjp/+b+I6+Mf11sY8nPae3XBIRqV27toqDg4ONnP4Zmz9/fiNntyXVf4OFhIQYubp166p40aJFRi69tv/hjC0AAAAAwNWY2AIAAAAAXI2JLQAAAADA1TJ0ja1dpzBnzhwV27UI+mW27XXm1JHcGvuy5L1791bx/fffb+T0Gq99+/YZuV69eqnYvvx9embXp1yXUetP7DpIu0ZI98MPPxjbe/fu9cWQICLlypVTsV0Hrdc6//rrr0bOF5+H9mdGgwYNVGzX/OlOnjxpbGfUz2q75cZ3332n4ho1ahi5/v37q1i//oGIeV0Du75Kr/+qVKmSkbNrdfX2UXb9rf75uGXLFiNH65DUYV/bwlO7ELul19dff63i9FqD5wZ2HeTy5cuTvK9+zHzyySdGTq+9tH/zFilSRMXdunUzcnptpYj5OXvkyBEjp7e81Ns9iZjHbEb9jXMz+veSXkstYv4+sn8f63XP9u8ofX/ZbZ7WrVtnbOvXWdDfEyIiPXv2VPHq1auN3IkTJyQ94owtAAAAAMDVmNgCAAAAAFyNiS0AAAAAwNUyVI2tXSfWqlUrY1vvp2fXjei9GqnzuT0RERHG9quvvqpiu04gNjZWxePHjzdyeo9Du4bLruVIy9qOpGpq8S+7X55e32XXTk+ZMsXYvnz5su8GlsHYn496XZ39Htb3i13H6ovx3HHHHUauYcOGN7yfiHmsHzx40Mhl1D6odm2xXgfZtWtXI1etWjUVz5w508idP39exfbrrtd06X1yRRLXxmfNmlXF9mfz1atXVfzHH38YuYxaI50a9GO8atWqRk6vtbb31/bt241t/boH1FSmDv111o8fEZEzZ84k+Xf6tRLsenb7OjP643700UdG7vvvv1expz6suDH9NdL7iIuILFu2TMVt27Y1cvbvZZ1eV2vXZ9ufq3oP44cfftjIlShRQsX33Xefkfv8889VnJ4+mzljCwAAAABwNSa2AAAAAABXy1BLkcPCwoztt99+29gOCgpS8dSpU43cxo0bVczSilunL3Nq0qSJkfO0zGn+/PkqXrFihZHTl6Ha7UDsJYf649otCLzRksC+jL6+RMReLhITE6Nie1l7Rnlv6a9X3759jZy+xNFeHrV27VrfDiwDs5eW6p+Hnpbw2u99/Vj39H62lzfbz6+XLAwfPtzI6S0J7MfRj6mFCxcmmcvIDhw4oOIRI0YYudGjR6s4MjLSyIWEhKjYbkeil4YsWbLEyK1cudLYvueee1Rs7z/9PWO3koLv6Mdx586djZx+bNqfBS+88IKxTXmIe+jLRwsVKuTxvnprlzfeeMPIuandYnqnl3uIiLz44osqfvfdd41cVFSUiu3vT70Mx27PZC9X11sM6UuPRUQaNWqk4jZt2hi5r776SsVnz56V9IIztgAAAAAAV2NiCwAAAABwNSa2AAAAAABX8/saW70FwVNPPWXk8uXLZ2zrNQSTJk0yct6ow8zI9P3QunVrI6e36rHbLC1dulTFdm2PpxpA+9LjensJm17T4Gk/2y2F8ubNq+IOHToYuSpVqqjY/je99tprKtZbpTiOk2HakRQvXlzFzZo1M3L6Ppg9e7aRo0bSd+xaR/340o9fEZFcuXKp2FPdjV2HqddP5smTx8jZLUZ69+6tYr3OR8SsB7TreHfs2HHDsYikr5YEaUl/HexWEOvXr1dxjRo1jJx+nQq7ZYTeXsL+zLNfd/2aCHZO3/bUqgTepbd3qVSpkpHTPxuOHTtm5P7880/fDgw+ox+HLVq0MHJ2rfSaNWtUfOrUKd8ODIrePsk+1uxWW0m5WctJ/bet3ZrtgQceUPHdd99t5OrVq6divS2RSNpeL4YztgAAAAAAV2NiCwAAAABwNb9bimyfcteX1NhtRexln6NGjVLxP//844PRZVz6Ugd9yZOIuWTBXr6gt/zQ2+SImMvU9GWTImZbChFzuXGBAgWSHKf9ntCXq1erVs3INW/eXMX2pfL1f++2bduMnH6J9p9++knF/rwU2b4U/WOPPaZiuyQgNjZWxd98842RyyjtkNKCvfRMb7VUv359I6cvwx87dqyRu+uuu274GCLm0je7JKFy5crGtr7c2W7npb8P9OX8ImZrIP29hBuzyy/018xepuxpSZunY9Mu4zh9+rSK7aXI+meg/XfwHf07zP5M1vfRBx98YORo7+Ne4eHhKs6fP7+Rsz8XaLWX/nirRFL/XN+wYYORO3TokIpLlixp5GrXrq3ib7/91sjpS6hTG98aAAAAAABXY2ILAAAAAHA1JrYAAAAAAFfzuxpbu9ZSr5u1c/Zacr21CG0hvEtfw3/16lUjp7/WehsPEZGnn35axR07dkzy8fWaP5HENXl79uxRsV2XoNd7RUZGGrlixYolObbg4GAV2++Xc+fO3fDxRcxL5et/58/1o3a7GP0S8nZOr5nUW3DBt+zjQq9v7tmzp5HLmTOnivU6LRGR7t27q1ivbRcxj0u71t4+vnT2saHX1+vts0REvv/+exXbnzW4PSn9jLL/Tn/P2J+dBw8eVDFtRVLPvffeq2L7WNTraO0WbP78veVv7Br50qVLq9huv2bz1P6Ndpjuph/D9m+uX3/9VcX27+MKFSqo2P4doNfmpvZnBGdsAQAAAACuxsQWAAAAAOBqfrEUWV9e0aJFCyNXs2ZNFduXnx4zZoyxHR8f74PRQcRcyjRnzhwjV6pUKRXnzp3byBUvXlzF9pI1fbmUvdTBXnKjL4G03wc5cuRQsd1ux9PySP2+9mNu2rRJxUuXLjVy+hIN/e/8eUmXvTRcv2y83dJj3759Kr506ZJPx4Wk/fjjjyr+7LPPjNzjjz+uYv34ETGXGNvLjfV97al1jIh5fOmlBCIi7733nopnzpxp5PgcT3/sY7xs2bIqtluB6cuPPX3+4vbYx1/Lli1VbO8TvZyG8hD3so9DvS2iXTZi/97S2695Wqruz79jMgK9jE7EbONjz6/03+7ly5c3ckeOHFFxapcEccYWAAAAAOBqTGwBAAAAAK7GxBYAAAAA4Gp+UWOrr/0fOXKkkdMvUa63gRAR+eWXX4xtagN8R6/X+OCDD4zcmjVrVNynTx8jV7lyZRXb9Xp6bZ99uXl7+/fff1ex3ipERKRAgQJJPoe+rY9TxGxLo8ciIj///LOK9VoDkYxZN2q/rnq7ALv+4qefflIxbbfSjv4+HTRokJHT6+x69epl5PTPY7tWTz8u7f1ut8X68ssvVTx16lQjt3PnThVfuHDByPE5nv7Y7UGioqJUrNfniZjvO/tzXK8LZT/fHrtOUq97th04cEDFtNByL081tvZxmDVrVmO7bt26Krava6BfK8Q+ZjlO3cU+vrdv365i+/oVeoufGjVqGDl9fmX/5vb17zrO2AIAAAAAXI2JLQAAAADA1Vy5FNle3ta+fXsV6+1hRMxLmL/99ttGzr6sNVKHveRl69atKn7++eeNnL6MxV7Soi+rsXOetu02B/rj2Et1PD2/p8fUcyynFYmNjTW2J06cqOLChQsbua+++krF9rImpA17mfCoUaNUPG/ePCOntw0pVKiQkdOXrP35559GTl++L2Iuf7SX7/O+cBf7s1Pf93FxcUnel/3sO/Z3nb701P7O0j+/7SXMGbG0xq08/Raxy4WCg4ON7RIlSqi4SZMmRk7/DvDUNojfQu6jt1/89NNPjVzz5s1VbM+n9M+Jm7X28zbO2AIAAAAAXI2JLQAAAADA1ZjYAgAAAABczZU1tqGhocZ29+7dVWzXjeitQzZs2GDkuAx5+nMrNVUprb+y97v+ONR0+YZdVz1+/Pg0Ggm8Qd+fdq2svQ1cuXLF2P7iiy9UrLfkExHZuHGjiu02EXxne4/d1mPTpk0qtuskp0yZomK9Th7uYv++0T+r9+7da+T0llwiZn1swYIFjZzeetFuv6bzdC0SpE/658Q333xj5PT3xMGDB40cNbYAAAAAAKQQE1sAAAAAgKu5ZimyvsS4adOmRq5cuXIqtpfQrFmzRsWelkgAAADvs5ccHj58WMV2Gz5PLdbgPfZS5Mcff1zF9tJBSnT802+//aZivXWLiEiFChWM7WPHjqnYXnaqL0+npY9/0T+D4+PjjdzOnTtVbLd01D9fUvs9wRlbAAAAAICrMbEFAAAAALgaE1sAAAAAgKul2xpbu8YjZ86cKq5YsaKR09eA67U7IiI//vijiqkTAQAg/aAmL31gP2Q8+m9nu0ZSvz4NMi79PXL06FEjp1/TyL6+kd6qjRpbAAAAAABuARNbAAAAAICrpdulyLZz586p+M033zRyH330kYpPnTpl5E6ePKliltoAAAAAgGf6vOns2bNGLi4u7ob3S2ucsQUAAAAAuBoTWwAAAACAqyVrKbJ+VazUYj+nvm2f8tavdmzn0mLsqcFb/y5/fX3czBv7hP2a/nDM+i/2rf9i3/ovvmv9E8esb3h6PVLrtUrO8yRrYquvo04r+oTVviy5vZ0RxMXFSWhoqFceB+mLN/Yt+zX94Zj1X+xb/8W+9V981/onjlnf8HTCMbUkZ98GOMkYWUJCghw+fFhCQkIS9ZdF6nIcR+Li4qRw4cISGHj7K8nZt+mHN/ct+zX94Jj1X+xb/8W+9V981/onjln/dSv7NlkTWwAAAAAA0isuHgUAAAAAcDUmtgAAAAAAV2NiCwAAAABwNSa2AAAAAABXY2ILAAAAAHA1JrYAAAAAAFdjYgsAAAAAcLX/B+YijQzOwpOEAAAAAElFTkSuQmCC",
      "text/plain": [
       "<Figure size 1200x400 with 16 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "best_model = MHVAE(img_size=g_opt.img_size, latent_dim1=g_opt.latent_dim1, latent_dim2=g_opt.latent_dim2)\n",
    "best_model.load_state_dict(torch.load(f\"{g_opt.save_dir}/best.pth\")['model_state_dict'])\n",
    "best_model.to(g_opt.device)\n",
    "best_model.eval()\n",
    "\n",
    "images, labels = next(iter(val_loader))\n",
    "images = images.to(g_opt.device)\n",
    "\n",
    "# 使用模型进行重建（不计算梯度）\n",
    "with torch.no_grad():\n",
    "    output = model(images)\n",
    "    reconstructed_images = output[0]\n",
    "\n",
    "images = images.cpu()\n",
    "reconstructed_images = reconstructed_images.cpu()\n",
    "\n",
    "def show_reconstructions(original, reconstructed, n=8):\n",
    "    plt.figure(figsize=(12, 4))\n",
    "    for i in range(n):\n",
    "        # 显示原始图片\n",
    "        ax = plt.subplot(2, n, i + 1)\n",
    "        plt.imshow(original[i].squeeze(), cmap='gray')\n",
    "        ax.get_xaxis().set_visible(False)\n",
    "        ax.get_yaxis().set_visible(False)\n",
    "        if i == n // 2:\n",
    "            ax.set_title('Original Images')\n",
    "        \n",
    "        # 显示重建图片\n",
    "        ax = plt.subplot(2, n, i + 1 + n)\n",
    "        plt.imshow(reconstructed[i].squeeze(), cmap='gray')\n",
    "        ax.get_xaxis().set_visible(False)\n",
    "        ax.get_yaxis().set_visible(False)\n",
    "        if i == n // 2:\n",
    "            ax.set_title('Reconstructed Images')\n",
    "    plt.show()\n",
    "\n",
    "# 展示前8张图片的重建效果\n",
    "print(\"测试集图像重建效果：\")\n",
    "show_reconstructions(images, reconstructed_images, n=8)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
